const fs = require('fs-extra');
const isEqual = require('lodash/isEqual');
const path = require('path');
const mapValues = require('lodash/mapValues');
const assign = require('lodash/assign');
const YAML = require('yaml');
const paths = require('./paths');
const isArray = require('lodash/isArray');

// Make sure that including paths.js after env.js will read .env variables.
const targetFile = require.resolve('./paths');
const cacheModule = require.cache[targetFile];
// 去除掉在require.cache缓存中parent对当前模块的引用，否则会引起内存泄露，具体解释可以看下面的文章
//《记录一次由一行代码引发的“血案”》https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63
//《一行 delete require.cache 引发的内存泄漏血案》https://zhuanlan.zhihu.com/p/34702356
if (cacheModule.parent) {
  cacheModule.parent.children.splice(
    cacheModule.parent.children.indexOf(cacheModule),
    1
  );
}
// @ts-ignore 清除指定路径对应模块的require.cache缓存
require.cache[targetFile] = null;
delete require.cache[targetFile];

const getEnvironment = () => {
  const NODE_ENV = process.env.NODE_ENV;
  const NODE_PROFILE = process.env.NODE_PROFILE;

  if (!NODE_ENV) {
    throw new Error(
      'The NODE_ENV environment variable is required but was not specified.'
    );
  }
  const getDotenvFiles = (dotenvPath) => {
    return [
      // Don't include `.env.local` for `test` environment
      // since normally you expect tests to produce the same
      // results for everyone
      dotenvPath,
      NODE_ENV !== 'test' && `${dotenvPath}.local`,
      NODE_PROFILE && `${dotenvPath}.${NODE_PROFILE}`,
      NODE_PROFILE && `${dotenvPath}.${NODE_PROFILE}.local`,
    ].filter(Boolean);
  };
  // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
  const dotenvFiles = getDotenvFiles(paths.dotenv);

  // Load environment variables from .env* files. Suppress warnings using silent
  // if this file is missing. dotenv will never modify any environment variables
  // that have already been set.  Variable expansion is supported in .env files.
  // https://github.com/motdotla/dotenv
  // https://github.com/motdotla/dotenv-expand
  dotenvFiles.forEach((dotenvFile) => {
    if (fs.existsSync(dotenvFile)) {
      require('dotenv-expand')(
        require('dotenv').config({
          path: dotenvFile,
        })
      );
    }
  });

  let defaultEnvConfigFile;
  try {
    defaultEnvConfigFile = path.resolve(
      path.dirname(require.resolve('cpaas-front/package.json')),
      'lib/config/.env.yml'
    );
  } catch (e) {
    defaultEnvConfigFile = path.resolve(
      paths.appRootPath,
      'node_modules/cpaas-front/lib/config/.env.yml'
    );
  }

  const applyYmlEnvFile = (yamlConfigFile) => {
    if (fs.existsSync(yamlConfigFile)) {
      const file = fs.readFileSync(yamlConfigFile, 'utf8');
      const _configObj = YAML.parse(file);
      if (typeof _configObj === 'object') {
        configObj = assign(configObj, _configObj);
      }
    }
  };

  let configObj = {};
  let envObj = {};
  [defaultEnvConfigFile]
    .concat(dotenvFiles.map((f) => `${f}.yml`))
    .forEach(applyYmlEnvFile);

  // 处理子模块配置文件
  if (paths.appRootPath !== paths.appPath) {
    getDotenvFiles(paths.appDotenv)
      .map((f) => `${f}.yml`)
      .forEach(applyYmlEnvFile);
  }

  if (Object.keys(configObj).length >= 1) {
    const templateVarReg = /(?<!\\)\$\{([A-Za-z_\-0-9]+)(:.+)?\}/g;
    /**
     * 处理 ${xxx:defaultVal} 模块变量
     * @param {string} val
     */
    const handleEnvStr = (val, result) => {
      if (templateVarReg.test(val)) {
        return val.replace(templateVarReg, (_, envName, defaultValue) => {
          defaultValue = defaultValue && defaultValue.substring(1);
          return process.env[envName] || result[envName] || defaultValue;
        });
      }
      return val;
    };
    const handleEnvValue = (val, result) => {
      let resultVal = val;
      if (typeof val === 'string') {
        return handleEnvStr(val, result);
      } else if (typeof val === 'object') {
        if (isArray(val)) {
          resultVal = val.map(handleEnvValue);
        } else {
          resultVal = mapValues(val, (childrenVal) => {
            if (typeof childrenVal === 'string') {
              return handleEnvStr(childrenVal, result);
            } else if (typeof childrenVal === 'object') {
              return handleEnvValue(childrenVal, result);
            }
            return childrenVal;
          });
        }
      }
      return JSON.stringify(resultVal);
    };
    envObj = Object.keys(configObj).reduce((result, configKey) => {
      const value = handleEnvValue(configObj[configKey], result);
      result[configKey] = value;
      return result;
    }, {});
    Object.keys(envObj).forEach((key) => {
      if (!process.env[key]) {
        process.env[key] = envObj[key];
      }
    });
  }

  // We support resolving modules according to `NODE_PATH`.
  // This lets you use absolute paths in imports inside large monorepos:
  // https://github.com/facebook/create-react-app/issues/253.
  // It works similar to `NODE_PATH` in Node itself:
  // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
  // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
  // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
  // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
  // We also resolve them to make sure all tools using them work consistently.
  const appDirectory = fs.realpathSync(process.cwd());
  process.env.NODE_PATH = (process.env.NODE_PATH || '')
    .split(path.delimiter)
    .filter((folder) => folder && !path.isAbsolute(folder))
    .map((folder) => path.resolve(appDirectory, folder))
    .join(path.delimiter);
  return envObj;
};

function getClientEnvironment(publicUrl) {
  const ENV_BASE_PATH = `${process.env.BASE_PATH}`.trim();
  const BASE_PATH = ENV_BASE_PATH.length === 0 ? '/' : ENV_BASE_PATH;
  const appPackageJson = require(paths.appPackageJson);
  const packageName = appPackageJson.name;
  const PACKAGE_NAME = packageName;
  let appName = 'cpaas-front';
  const APP_NAME = process.env.APP_NAME ? process.env.APP_NAME : appName;
  const appRootPackageFile = path.resolve(paths.appRootPath, 'package.json');
  if (fs.existsSync(appRootPackageFile)) {
    const appRootPackageJson = require(path.resolve(
      paths.appRootPath,
      'package.json'
    ));
    appName = appRootPackageJson.name;
  }

  const API_HOST = process.env.API_HOST ? process.env.API_HOST : '';
  const PACKAGE_PUBLIC_URL = process.env.PACKAGE_PUBLIC_URL;

  const WEBSOCKET_HOST = process.env.WEBSOCKET_HOST || '';
  const CLIENT_ID = process.env.CLIENT_ID ? process.env.CLIENT_ID : '';
  const AUTH_HOST = `${API_HOST}/oauth`;
  const PLATFORM_VERSION = process.env.PLATFORM_VERSION || 'SAAS';
  const IM_ENABLE = process.env.IM_ENABLE || 'false';
  let CLIENT_ENV_LIST = null;
  if (process.env.CLIENT_ENV_LIST) {
    try {
      CLIENT_ENV_LIST = JSON.parse(process.env.CLIENT_ENV_LIST);
    } catch (e) {
      console.log(e);
    }
  }
  if (CLIENT_ENV_LIST === null) {
    CLIENT_ENV_LIST = [
      'BASE_PATH',
      'PLATFORM_VERSION',
      'API_HOST',
      'WEBSOCKET_HOST',
      'PUBLIC_URL',
      'CUSTOMIZE_ICON_NAME',
      'TRACE_LOG_ENABLE',
      'IM_WEBSOCKET_HOST',
      'IM_ENABLE',
    ];
  }

  const raw = Object.keys(process.env)
    .filter((key) => CLIENT_ENV_LIST.includes(key))
    .reduce(
      (env, key) => {
        env[key] = process.env[key];
        return env;
      },
      {
        // Useful for determining whether we’re running in production mode.
        // Most importantly, it switches React into the correct mode.
        NODE_ENV: process.env.NODE_ENV || 'development',
        // Useful for resolving the correct path to static assets in `public`.
        // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
        // This should only be used as an escape hatch. Normally you would put
        // images into the `src` and `import` them in code to get their paths.
        PUBLIC_URL: publicUrl,

        BASE_PATH,
        API_HOST,
        PACKAGE_PUBLIC_URL,
        APP_NAME,
        PACKAGE_NAME,
        WEBSOCKET_HOST,
        CLIENT_ID,
        AUTH_HOST,
        PLATFORM_VERSION,
        IM_ENABLE,

        // SERVICE CONFIG
        // 服务合并的环境变量
        routeMap: process.env.routeMap,
      }
    );
  // Stringify all values so we can feed into Webpack DefinePlugin
  const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {}),
  };

  return {
    raw,
    stringified,
  };
}

const envData = getEnvironment();
const { getCachedEnv } = require('../utils/cache');

/**
 * 自从上次编译之后环境变量是否发生变化
 */
const envIsChanged = async () => {
  const cachedEnvData = await getCachedEnv();
  if (!cachedEnvData) {
    return true;
  }

  const _envData = {
    ...envData,
  };
  const _cachedEnvData = {
    ...cachedEnvData,
  };
  if (_envData.routeMap) {
    _envData.routeMap = JSON.parse(_envData.routeMap);
  }
  if (_cachedEnvData.routeMap) {
    _cachedEnvData.routeMap = JSON.parse(_cachedEnvData.routeMap);
  }

  if (isEqual(_envData, _cachedEnvData)) {
    return false;
  }

  return true;
};

module.exports.getClientEnvironment = getClientEnvironment;
module.exports.getEnvironment = getEnvironment;
module.exports.envIsChanged = envIsChanged;
module.exports.envData = envData;
